package CertificateAuthority;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPublicKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Calendar;
import java.util.Date;

import sun.security.rsa.RSAKeyFactory;

import javacard.security.RSAPublicKey;


public class Cert {
	
	private byte domain;
	private byte[] issuer;
	private byte[] subject;
	public PublicKey publicKey;		//TODO: must become private! Public because other certificates have to be signed by this key from the CA
	public PrivateKey privateKey;	//TODO: must become private! Public because other certificates have to be signed by this key from the CA
	public byte[] signature;
	public byte[] endDate;
	
	public byte GetDomain() {
		return this.domain;
	}
	
	public String Getsubject() {
		return new String(this.subject);
	}
	
	//Must be commented out in the future! You shouldn't be able to make new certificates once we have them all.
	public Cert(byte domain, String issuer, String subject, Date endDate) {
		this.issuer = new byte[issuer.getBytes().length + 1];
		System.arraycopy(issuer.getBytes(), 0, this.issuer, 0, issuer.getBytes().length);
		this.issuer[this.issuer.length-1] = 0;
		
		this.subject = new byte[subject.getBytes().length+1];
		System.arraycopy(subject.getBytes(), 0, this.subject, 0, subject.getBytes().length);
		this.subject[this.subject.length-1] = 0;
		
		fillEndDateByteArray(endDate);

		
		this.domain = domain;
		KeyPairGenerator kpg;
		try {
			kpg = KeyPairGenerator.getInstance("RSA");
			kpg.initialize(512);
	        KeyPair kp = kpg.genKeyPair();
	        this.publicKey = kp.getPublic();
	        this.privateKey = kp.getPrivate(); 
		} catch (NoSuchAlgorithmException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	//Must be private in the future! You shouldn't be able to make new certificates once we have them all.
	private Cert(PublicKey publicKey, byte domain, byte[] issuer, byte[] subject, byte[] endDate, byte[] signature ) {
		this.issuer = issuer;
		this.subject = subject;
		this.domain = domain;
		this.endDate = endDate;
		this.publicKey = publicKey;
		this.signature = signature;
	}	
	
	//Must be commented out in the future! You shouldn't be able to make new certificates once we have them all.
	public void ExportPrivateToFile(String filename) {
		FileOutputStream fstream;
		try {
			fstream = new FileOutputStream(filename);
			fstream.write(this.privateKey.getEncoded());
		} catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	/**
	 * 
	 * @param certificate the byte representation of a certificate ( custom protocol ) 
	 * @return Cert object
	 * @throws InvalidKeySpecException
	 * @throws NoSuchAlgorithmException
	 */
	public static Cert FromByteArray(byte[] certificate) throws InvalidKeySpecException, NoSuchAlgorithmException {
		//Domain ID - 1 byte
		//subject - x bytes
		//issuer - y bytes
		//end year - 2 bytes
		//end month - 1 byte
		//end day - 1 byte
		//Public key modulus length - 2 bytes (short)
		//Public key exponent length - 2 bytes (short)
		//Public key modulus
		//Public key exponent
		//Signature length - 2 bytes (short)
		//Signature
		
		short offset = 0;
		byte domainID = certificate[offset];
		offset++;
		
		/***** Read subject *****/
		byte[] subject = null;
		while(certificate[offset] != 0 ) {
			subject = addByte(subject, certificate[offset]);
			offset++;
		}
		subject = addByte(subject, certificate[offset]); //Add the last 0
		offset++;
		
		/***** Read issuer *****/
		byte[] issuer = null;
		while(certificate[offset] != 0 ) {
			issuer = addByte(issuer, certificate[offset]);
			offset++;
		}
		issuer = addByte(issuer, certificate[offset]); //Add the last 0
		offset++;
		
		/***** Read End Date *****/
		byte[] endDate = new byte[4];
		System.arraycopy(certificate, offset, endDate, 0, 4);	//endDate = 4 bytes (2 bytes year, 1 byte month and 1 byte day)
		offset += 4;
		
		/***** Read key length *****/
		short publicKeyModulusLength = (short) (((certificate[offset] << 8)) | ((certificate[offset + 1] & 0xff)));
		offset += 2;
		short publicKeyExponentLength = (short) (((certificate[offset] << 8)) | ((certificate[offset + 1] & 0xff)));
		offset += 2;
		
		byte[] publicKeyModulus = new byte[publicKeyModulusLength];
		byte[] publicKeyExponent = new byte[publicKeyExponentLength];
		
		/***** Copy key *****/
		System.arraycopy(certificate, offset, publicKeyModulus, 0, publicKeyModulusLength);
		offset += publicKeyModulusLength;
		System.arraycopy(certificate, offset, publicKeyExponent, 0, publicKeyExponentLength);
		offset += publicKeyExponentLength;
		
		/***** Read signature length *****/
		short signatureLength = (short) (((certificate[offset] << 8)) | ((certificate[offset + 1] & 0xff)));
		offset += 2;
		
		/***** Read signature *****/
		byte[] signature = new byte[signatureLength];
		System.arraycopy(certificate, offset, signature, 0, signatureLength);
		
		KeyFactory factory = KeyFactory.getInstance("RSA");
		RSAPublicKeySpec keySpec = new RSAPublicKeySpec(new BigInteger(publicKeyModulus), new BigInteger(publicKeyExponent));
		PublicKey myPublicKey = factory.generatePublic(keySpec);
		
		return new Cert(myPublicKey, domainID, issuer, subject, endDate, signature);
	}
	
	//Must be commented out in the future! You shouldn't be able to make new certificates once we have them all.
	public void Sign(PrivateKey CAPrivateKey) throws NoSuchAlgorithmException, NoSuchProviderException, SignatureException, InvalidKeyException {        
        Signature rsa = Signature.getInstance("SHA1withRSA");
        rsa.initSign(CAPrivateKey);
        /* All attributes should be signed, except for the private key and signature */
        rsa.update(this.getBytesCertForCard(false));
        this.signature = rsa.sign();
	}
	
	public byte[] getBytesCertForCard(boolean includeSignature) {
		//Domain ID - 1 byte
		//subject - x bytes
		//issuer - y bytes
		//end year - 2 bytes
		//end month - 1 byte
		//end day - 1 byte
		//Public key modulus length - 2 bytes (short)
		//Public key exponent length - 2 bytes (short)
		//Public key modulus
		//Public key exponent
		//Signature length - 2 bytes (short)
		//Signature
		
		ByteArrayOutputStream bos = new ByteArrayOutputStream();
		DataOutputStream out = new DataOutputStream(bos);
		try {
			out.writeByte(this.domain);
			out.write(this.subject, 0, this.subject.length);
			out.write(this.issuer, 0, this.issuer.length);
			out.write(this.endDate, 0, this.endDate.length);
			
			sun.security.rsa.RSAPublicKeyImpl RSApublicKey = (sun.security.rsa.RSAPublicKeyImpl)this.publicKey;
			byte[] publicKeyModulus = RSApublicKey.getModulus().toByteArray();
			byte[] publicKeyExponent = RSApublicKey.getPublicExponent().toByteArray();
			
			out.writeShort(publicKeyModulus.length);
			out.writeShort(publicKeyExponent.length);
			
			out.write(publicKeyModulus, 0, publicKeyModulus.length);
			out.write(publicKeyExponent, 0, publicKeyExponent.length);
			if(includeSignature) out.writeShort(this.signature.length);
			if(includeSignature) out.write(this.signature, 0, this.signature.length);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		return bos.toByteArray();
	}
	
	/**
	 * 
	 * @param CAPublicKey Public key from the CA ( or the authority who signed the certificate )
	 * @return	true if the verification succeeded
	 */
	public boolean Verify(PublicKey CAPublicKey) throws NoSuchAlgorithmException {
		try {
			Signature rsa = Signature.getInstance("SHA1withRSA");
			rsa.initVerify(CAPublicKey);
			rsa.update(this.getBytesCertForCard(false));
			return rsa.verify(this.signature);
		} catch (InvalidKeyException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (SignatureException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return false;
	}
	
	public boolean ExportToFile(String filename) {
		FileOutputStream fstream;
		try {
			byte[] output = getBytesCertForCard(true);
			fstream = new FileOutputStream(filename);
			fstream.write(output);
		} catch (FileNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			return false;
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			return false;
		}
		return true;		 
	}
	
	public String toString() {
		StringBuffer output = new StringBuffer();
        
		output.append("Certificate info:\n" );
		output.append("domain = [" + this.domain + "]\n");
		output.append("issuer = [" + new String(this.issuer) + "]\n");
		output.append("subject = [" + new String(this.subject) + "]\n");
		output.append("public key = [" + this.publicKey.getEncoded() + "]\n");
        output.append("signature = [");
        for(int i=0; i<this.signature.length; i++) {
        	output.append(Integer.toHexString(this.signature[i]));
        }
        output.append("]\n");
        return output.toString();
	}
	
	private static byte[] addByte(byte[] array, byte b){
		if(array == null ) {
			byte[] out = new byte[1];
			out[0] = b;
			return out;
		}
		else{
			byte[] out = new byte[array.length+1];
			System.arraycopy(array, 0, out, 0, array.length);
			out[out.length-1] = b;
			return out;
		}
	}
	
	private void fillEndDateByteArray(Date endDate) {
//				+------+-----+---+
//				| year |month|day|
//endDate  =    |------|-----|---|
//				| b0 b1| b2  |b3 | 
//				+------+-----+---+
	try {
		Calendar c = Calendar.getInstance();
		c.setTime(endDate);
		ByteArrayOutputStream bos = new ByteArrayOutputStream();
		DataOutputStream out = new DataOutputStream(bos);
		
		out.writeShort(c.get(Calendar.YEAR));
		out.writeByte(c.get(Calendar.MONTH));
		out.writeByte(c.get(Calendar.DAY_OF_MONTH));
		this.endDate = bos.toByteArray();
	} catch (IOException e1) {
		// TODO Auto-generated catch block
		e1.printStackTrace();
	}

}
}
